// ========================================================================
// Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
// You may elect to redistribute this code under either of these licenses.
// ========================================================================

package org.eclipse.jetty.http;

import java.io.UnsupportedEncodingException;
import java.net.URI;

import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.Utf8StringBuilder;

/* ------------------------------------------------------------ */
/**
 * Http URI. Parse a HTTP URI from a string or byte array. Given a URI <code>http://user@host:port/path/info;param?query#fragment</code> this class will split it into the following undecoded optional elements:
 * <ul>
 * <li>{@link #getScheme()} - http:</li>
 * <li>{@link #getAuthority()} - //name@host:port</li>
 * <li>{@link #getHost()} - host</li>
 * <li>{@link #getPort()} - port</li>
 * <li>{@link #getPath()} - /path/info</li>
 * <li>{@link #getParam()} - param</li>
 * <li>{@link #getQuery()} - query</li>
 * <li>{@link #getFragment()} - fragment</li>
 * </ul>
 */
@SuppressWarnings("rawtypes")
public class HttpURI
{

	private static final byte[] __empty = {};
	private final static int
		START = 0,
		AUTH_OR_PATH = 1,
		SCHEME_OR_PATH = 2,
		AUTH = 4,
		IPV6 = 5,
		PORT = 6,
		PATH = 7,
		PARAM = 8,
		QUERY = 9,
		ASTERISK = 10;

	boolean _partial = false;
	byte[] _raw = __empty;
	String _rawString;
	int _scheme;
	int _authority;
	int _host;
	int _port;
	int _portValue;
	int _path;
	int _param;
	int _query;
	int _fragment;
	int _end;
	boolean _encoded = false;

	final Utf8StringBuilder _utf8b = new Utf8StringBuilder(64);

	public HttpURI()
	{

	}

	/* ------------------------------------------------------------ */
	/**
	 * @param parsePartialAuth If True, parse auth without prior scheme, else treat all URIs starting with / as paths
	 */
	public HttpURI(boolean parsePartialAuth)
	{
		_partial = parsePartialAuth;
	}

	public HttpURI(String raw)
	{
		_rawString = raw;
		byte[] b = raw.getBytes();
		parse(b, 0, b.length);
	}

	public HttpURI(byte[] raw, int offset, int length)
	{
		parse2(raw, offset, length);
	}

	public HttpURI(URI uri)
	{
		parse(uri.toASCIIString());
	}

	public void parse(String raw)
	{
		byte[] b = raw.getBytes();
		parse2(b, 0, b.length);
		_rawString = raw;
	}

	public void parse(byte[] raw, int offset, int length)
	{
		_rawString = null;
		parse2(raw, offset, length);
	}

	public void parseConnect(byte[] raw, int offset, int length)
	{
		_rawString = null;
		_encoded = false;
		_raw = raw;
		int i = offset;
		int e = offset + length;
		int state = AUTH;
		@SuppressWarnings("unused")
		int m = offset;
		_end = offset + length;
		_scheme = offset;
		_authority = offset;
		_host = offset;
		_port = _end;
		_portValue = -1;
		_path = _end;
		_param = _end;
		_query = _end;
		_fragment = _end;

		loop: while (i < e)
		{
			char c = (char)(0xff & _raw[i]);
			int s = i++;

			switch (state)
			{
				case AUTH: {
					switch (c)
					{
						case ':': {
							_port = s;
							break loop;
						}
						case '[': {
							state = IPV6;
							break;
						}
					}
					continue;
				}

				case IPV6: {
					switch (c)
					{
						case '/': {
							throw new IllegalArgumentException("No closing ']' for " + StringUtil.toString(_raw, offset, length, URIUtil.__CHARSET));
						}
						case ']': {
							state = AUTH;
							break;
						}
					}

					continue;
				}
			}
		}

		if (_port < _path)
			_portValue = TypeUtil.parseInt(_raw, _port + 1, _path - _port - 1, 10);
		else
			throw new IllegalArgumentException("No port");
		_path = offset;
	}

	private void parse2(byte[] raw, int offset, int length)
	{
		_encoded = false;
		_raw = raw;
		int i = offset;
		int e = offset + length;
		int state = START;
		int m = offset;
		_end = offset + length;
		_scheme = offset;
		_authority = offset;
		_host = offset;
		_port = offset;
		_portValue = -1;
		_path = offset;
		_param = _end;
		_query = _end;
		_fragment = _end;
		while (i < e)
		{
			char c = (char)(0xff & _raw[i]);
			int s = i++;

			state: switch (state)
			{
				case START: {
					m = s;
					switch (c)
					{
						case '/':
							state = AUTH_OR_PATH;
							break;
						case ';':
							_param = s;
							state = PARAM;
							break;
						case '?':
							_param = s;
							_query = s;
							state = QUERY;
							break;
						case '#':
							_param = s;
							_query = s;
							_fragment = s;
							break;
						case '*':
							_path = s;
							state = ASTERISK;
							break;

						default:
							state = SCHEME_OR_PATH;
					}

					continue;
				}

				case AUTH_OR_PATH: {
					if ((_partial || _scheme != _authority) && c == '/')
					{
						_host = i;
						_port = _end;
						_path = _end;
						state = AUTH;
					}
					else if (c == ';' || c == '?' || c == '#')
					{
						i--;
						state = PATH;
					}
					else
					{
						_host = m;
						_port = m;
						state = PATH;
					}
					continue;
				}

				case SCHEME_OR_PATH: {
					// short cut for http and https
					if (length > 6 && c == 't')
					{
						if (_raw[offset + 3] == ':')
						{
							s = offset + 3;
							i = offset + 4;
							c = ':';
						}
						else if (_raw[offset + 4] == ':')
						{
							s = offset + 4;
							i = offset + 5;
							c = ':';
						}
						else if (_raw[offset + 5] == ':')
						{
							s = offset + 5;
							i = offset + 6;
							c = ':';
						}
					}

					switch (c)
					{
						case ':': {
							m = i++;
							_authority = m;
							_path = m;
							c = (char)(0xff & _raw[i]);
							if (c == '/')
								state = AUTH_OR_PATH;
							else
							{
								_host = m;
								_port = m;
								state = PATH;
							}
							break;
						}

						case '/': {
							state = PATH;
							break;
						}

						case ';': {
							_param = s;
							state = PARAM;
							break;
						}

						case '?': {
							_param = s;
							_query = s;
							state = QUERY;
							break;
						}

						case '#': {
							_param = s;
							_query = s;
							_fragment = s;
							break;
						}
					}
					continue;
				}

				case AUTH: {
					switch (c)
					{

						case '/': {
							m = s;
							_path = m;
							_port = _path;
							state = PATH;
							break;
						}
						case '@': {
							_host = i;
							break;
						}
						case ':': {
							_port = s;
							state = PORT;
							break;
						}
						case '[': {
							state = IPV6;
							break;
						}
					}
					continue;
				}

				case IPV6: {
					switch (c)
					{
						case '/': {
							throw new IllegalArgumentException("No closing ']' for " + StringUtil.toString(_raw, offset, length, URIUtil.__CHARSET));
						}
						case ']': {
							state = AUTH;
							break;
						}
					}

					continue;
				}

				case PORT: {
					if (c == '/')
					{
						m = s;
						_path = m;
						if (_port <= _authority)
							_port = _path;
						state = PATH;
					}
					continue;
				}

				case PATH: {
					switch (c)
					{
						case ';': {
							_param = s;
							state = PARAM;
							break;
						}
						case '?': {
							_param = s;
							_query = s;
							state = QUERY;
							break;
						}
						case '#': {
							_param = s;
							_query = s;
							_fragment = s;
							break state;
						}
						case '%': {
							_encoded = true;
						}
					}
					continue;
				}

				case PARAM: {
					switch (c)
					{
						case '?': {
							_query = s;
							state = QUERY;
							break;
						}
						case '#': {
							_query = s;
							_fragment = s;
							break state;
						}
					}
					continue;
				}

				case QUERY: {
					if (c == '#')
					{
						_fragment = s;
						break state;
					}
					continue;
				}

				case ASTERISK: {
					throw new IllegalArgumentException("only '*'");
				}
			}
		}

		if (_port < _path)
			_portValue = TypeUtil.parseInt(_raw, _port + 1, _path - _port - 1, 10);
	}

	private String toUtf8String(int offset, int length)
	{
		_utf8b.reset();
		_utf8b.append(_raw, offset, length);
		return _utf8b.toString();
	}

	public String getScheme()
	{
		if (_scheme == _authority)
			return null;
		int l = _authority - _scheme;
		if (l == 5 &&
			_raw[_scheme] == 'h' &&
			_raw[_scheme + 1] == 't' &&
			_raw[_scheme + 2] == 't' &&
			_raw[_scheme + 3] == 'p')
			return HttpSchemes.HTTP;
		if (l == 6 &&
			_raw[_scheme] == 'h' &&
			_raw[_scheme + 1] == 't' &&
			_raw[_scheme + 2] == 't' &&
			_raw[_scheme + 3] == 'p' &&
			_raw[_scheme + 4] == 's')
			return HttpSchemes.HTTPS;

		return toUtf8String(_scheme, _authority - _scheme - 1);
	}

	public String getAuthority()
	{
		if (_authority == _path)
			return null;
		return toUtf8String(_authority, _path - _authority);
	}

	public String getHost()
	{
		if (_host == _port)
			return null;
		return toUtf8String(_host, _port - _host);
	}

	public int getPort()
	{
		return _portValue;
	}

	public String getPath()
	{
		if (_path == _param)
			return null;
		return toUtf8String(_path, _param - _path);
	}

	public String getDecodedPath()
	{
		if (_path == _param)
			return null;

		int length = _param - _path;
		byte[] bytes = null;
		int n = 0;

		for (int i = _path; i < _param; i++)
		{
			byte b = _raw[i];

			if (b == '%')
			{
				if ((i + 2) >= _param)
					throw new IllegalArgumentException("Bad % encoding: " + this);
				b = (byte)(0xff & TypeUtil.parseInt(_raw, i + 1, 2, 16));
				i += 2;
			}
			else if (bytes == null)
			{
				n++;
				continue;
			}

			if (bytes == null)
			{
				bytes = new byte[length];
				System.arraycopy(_raw, _path, bytes, 0, n);
			}

			bytes[n++] = b;
		}

		if (bytes == null)
			return toUtf8String(_path, length);

		_utf8b.reset();
		_utf8b.append(bytes, 0, n);
		return _utf8b.toString();
	}

	public String getPathAndParam()
	{
		if (_path == _query)
			return null;
		return toUtf8String(_path, _query - _path);
	}

	public String getCompletePath()
	{
		if (_path == _end)
			return null;
		return toUtf8String(_path, _end - _path);
	}

	public String getParam()
	{
		if (_param == _query)
			return null;
		return toUtf8String(_param + 1, _query - _param - 1);
	}

	public String getQuery()
	{
		if (_query == _fragment)
			return null;
		return toUtf8String(_query + 1, _fragment - _query - 1);
	}

	public String getQuery(String encoding)
	{
		if (_query == _fragment)
			return null;
		return StringUtil.toString(_raw, _query + 1, _fragment - _query - 1, encoding);
	}

	public boolean hasQuery()
	{
		return (_fragment > _query);
	}

	public String getFragment()
	{
		if (_fragment == _end)
			return null;
		return toUtf8String(_fragment + 1, _end - _fragment - 1);
	}

	public void decodeQueryTo(MultiMap parameters)
	{
		if (_query == _fragment)
			return;
		_utf8b.reset();
		UrlEncoded.decodeUtf8To(_raw, _query + 1, _fragment - _query - 1, parameters, _utf8b);
	}

	public void decodeQueryTo(MultiMap parameters, String encoding)
		throws UnsupportedEncodingException
	{
		if (_query == _fragment)
			return;

		if (encoding == null || StringUtil.isUTF8(encoding))
			UrlEncoded.decodeUtf8To(_raw, _query + 1, _fragment - _query - 1, parameters);
		else
			UrlEncoded.decodeTo(toUtf8String(_query + 1, _fragment - _query - 1), parameters, encoding);
	}

	public void clear()
	{
		_scheme = _authority = _host = _port = _path = _param = _query = _fragment = _end = 0;
		_raw = __empty;
		_rawString = "";
		_encoded = false;
	}

	@Override
	public String toString()
	{
		if (_rawString == null)
			_rawString = toUtf8String(_scheme, _end - _scheme);
		return _rawString;
	}

	public void writeTo(Utf8StringBuilder buf)
	{
		buf.append(_raw, _scheme, _end - _scheme);
	}

}
